{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import base64, requests, time, pytz, logging, os, sys, dotenv\n",
    "from datetime import datetime, timedelta\n",
    "from influxdb import InfluxDBClient\n",
    "from influxdb.exceptions import InfluxDBClientError\n",
    "import xml.etree.ElementTree as ET\n",
    "from garth.exc import GarthHTTPError\n",
    "from garminconnect import (\n",
    "    Garmin,\n",
    "    GarminConnectAuthenticationError,\n",
    "    GarminConnectConnectionError,\n",
    "    GarminConnectTooManyRequestsError,\n",
    ")\n",
    "garmin_obj = None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "banner_text = \"\"\"\n",
    "\n",
    "*****  █▀▀ ▄▀█ █▀█ █▀▄▀█ █ █▄ █    █▀▀ █▀█ ▄▀█ █▀▀ ▄▀█ █▄ █ ▄▀█  *****\n",
    "*****  █▄█ █▀█ █▀▄ █ ▀ █ █ █ ▀█    █▄█ █▀▄ █▀█ █▀  █▀█ █ ▀█ █▀█  *****\n",
    "\n",
    "______________________________________________________________________\n",
    "\n",
    "By Arpan Ghosh | Please consider supporting the project if you love it\n",
    "______________________________________________________________________\n",
    "\n",
    "\"\"\"\n",
    "print(banner_text)\n",
    "env_override = dotenv.load_dotenv(\"override-default-vars.env\", override=True)\n",
    "if env_override:\n",
    "    logging.warning(\"System ENV variables are overridden with override-default-vars.env\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 79,
   "metadata": {},
   "outputs": [],
   "source": [
    "INFLUXDB_HOST = os.getenv(\"INFLUXDB_HOST\",'your.influxdb.hostname') # Required\n",
    "INFLUXDB_PORT = int(os.getenv(\"INFLUXDB_PORT\", 8086)) # Required\n",
    "INFLUXDB_USERNAME = os.getenv(\"INFLUXDB_USERNAME\", 'influxdb_username') # Required\n",
    "INFLUXDB_PASSWORD = os.getenv(\"INFLUXDB_PASSWORD\", 'influxdb_access_password') # Required\n",
    "INFLUXDB_DATABASE = os.getenv(\"INFLUXDB_DATABASE\", 'GarminStats') # Required\n",
    "TOKEN_DIR = os.getenv(\"TOKEN_DIR\", \"~/.garminconnect\") # optional\n",
    "GARMINCONNECT_EMAIL = os.environ.get(\"GARMINCONNECT_EMAIL\", None) # optional, asks in prompt on run if not provided\n",
    "GARMINCONNECT_PASSWORD = base64.b64decode(os.getenv(\"GARMINCONNECT_BASE64_PASSWORD\")).decode(\"utf-8\") if os.getenv(\"GARMINCONNECT_BASE64_PASSWORD\") != None else None # optional, asks in prompt on run if not provided\n",
    "GARMIN_DEVICENAME = os.getenv(\"GARMIN_DEVICENAME\", \"Unknown\")  # optional, attepmts to set the same automatically if not given\n",
    "AUTO_DATE_RANGE = False if os.getenv(\"AUTO_DATE_RANGE\") in ['False','false','FALSE','f','F','no','No','NO','0'] else True # optional\n",
    "MANUAL_START_DATE = os.getenv(\"MANUAL_START_DATE\", None) # optional, in YYYY-MM-DD format, if you want to bulk update only from specific date\n",
    "MANUAL_END_DATE = os.getenv(\"MANUAL_END_DATE\", datetime.today().strftime('%Y-%m-%d')) # optional, in YYYY-MM-DD format, if you want to bulk update until a specific date\n",
    "LOG_LEVEL = os.getenv(\"LOG_LEVEL\", \"INFO\") # optional\n",
    "FETCH_FAILED_WAIT_SECONDS = int(os.getenv(\"FETCH_FAILED_WAIT_SECONDS\", 1800)) # optional\n",
    "RATE_LIMIT_CALLS_SECONDS = int(os.getenv(\"RATE_LIMIT_CALLS_SECONDS\", 5)) # optional\n",
    "INFLUXDB_ENDPOINT_IS_HTTP = False if os.getenv(\"INFLUXDB_ENDPOINT_IS_HTTP\") in ['False','false','FALSE','f','F','no','No','NO','0'] else True # optional\n",
    "GARMIN_DEVICENAME_AUTOMATIC = False if GARMIN_DEVICENAME != \"Unknown\" else True # optional\n",
    "UPDATE_INTERVAL_SECONDS = int(os.getenv(\"UPDATE_INTERVAL_SECONDS\", 300)) # optional"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "for handler in logging.root.handlers[:]:\n",
    "    logging.root.removeHandler(handler)\n",
    "\n",
    "logging.basicConfig(\n",
    "    level=getattr(logging, LOG_LEVEL, logging.INFO),\n",
    "    format=\"%(asctime)s - %(levelname)s - %(message)s\",\n",
    "    handlers=[\n",
    "        logging.StreamHandler(sys.stdout)\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    if INFLUXDB_ENDPOINT_IS_HTTP:\n",
    "        influxdbclient = InfluxDBClient(host=INFLUXDB_HOST, port=INFLUXDB_PORT, username=INFLUXDB_USERNAME, password=INFLUXDB_PASSWORD)\n",
    "    else:\n",
    "        influxdbclient = InfluxDBClient(host=INFLUXDB_HOST, port=INFLUXDB_PORT, username=INFLUXDB_USERNAME, password=INFLUXDB_PASSWORD, ssl=True, verify_ssl=True)\n",
    "    influxdbclient.switch_database(INFLUXDB_DATABASE)\n",
    "    influxdbclient.ping()\n",
    "except InfluxDBClientError as err:\n",
    "    logging.error(\"Unable to connect with influxdb database! Aborted\")\n",
    "    raise InfluxDBClientError(\"InfluxDB connection failed:\" + str(err))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def iter_days(start_date: str, end_date: str):\n",
    "    start = datetime.strptime(start_date, '%Y-%m-%d')\n",
    "    end = datetime.strptime(end_date, '%Y-%m-%d')\n",
    "    current = end\n",
    "\n",
    "    while current >= start:\n",
    "        yield current.strftime('%Y-%m-%d')\n",
    "        current -= timedelta(days=1)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def garmin_login():\n",
    "    try:\n",
    "        logging.info(f\"Trying to login to Garmin Connect using token data from directory '{TOKEN_DIR}'...\")\n",
    "        garmin = Garmin()\n",
    "        garmin.login(TOKEN_DIR)\n",
    "        logging.info(\"login to Garmin Connect successful using stored session tokens.\")\n",
    "\n",
    "    except (FileNotFoundError, GarthHTTPError, GarminConnectAuthenticationError):\n",
    "        logging.warning(\"Session is expired or login information not present/incorrect. You'll need to log in again...login with your Garmin Connect credentials to generate them.\")\n",
    "        try:\n",
    "            user_email = GARMINCONNECT_EMAIL or input(\"Enter Garminconnect Login e-mail: \")\n",
    "            user_password = GARMINCONNECT_PASSWORD or input(\"Enter Garminconnect password (characters will be visible): \")\n",
    "            garmin = Garmin(\n",
    "                email=user_email, password=user_password, is_cn=False, return_on_mfa=True\n",
    "            )\n",
    "            result1, result2 = garmin.login()\n",
    "            if result1 == \"needs_mfa\":  # MFA is required\n",
    "                mfa_code = input(\"MFA one-time code: \")\n",
    "                garmin.resume_login(result2, mfa_code)\n",
    "\n",
    "            garmin.garth.dump(TOKEN_DIR)\n",
    "            logging.info(f\"Oauth tokens stored in '{TOKEN_DIR}' directory for future use\")\n",
    "\n",
    "            garmin.login(TOKEN_DIR)\n",
    "            logging.info(\"login to Garmin Connect successful using stored session tokens.\")\n",
    "\n",
    "        except (\n",
    "            FileNotFoundError,\n",
    "            GarthHTTPError,\n",
    "            GarminConnectAuthenticationError,\n",
    "            requests.exceptions.HTTPError,\n",
    "        ) as err:\n",
    "            logging.error(str(err))\n",
    "            raise Exception(\"Session is expired : please login again and restart the script\")\n",
    "\n",
    "    return garmin"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "def write_points_to_influxdb(points):\n",
    "    try:\n",
    "        if len(points) != 0:\n",
    "            influxdbclient.write_points(points)\n",
    "            logging.info(\"Successfully updated influxdb database with new points\")\n",
    "    except InfluxDBClientError as err:\n",
    "        logging.error(\"Unable to connect with database! \" + str(err))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_daily_stats(date_str):\n",
    "    points_list = []\n",
    "    stats_json = garmin_obj.get_stats(date_str)\n",
    "    if stats_json['wellnessStartTimeGmt'] and datetime.strptime(date_str, \"%Y-%m-%d\") < datetime.today():\n",
    "        points_list.append({\n",
    "            \"measurement\":  \"DailyStats\",\n",
    "            \"time\": pytz.timezone(\"UTC\").localize(datetime.strptime(stats_json['wellnessStartTimeGmt'], \"%Y-%m-%dT%H:%M:%S.%f\")).isoformat(),\n",
    "            \"tags\": {\n",
    "                \"Device\": GARMIN_DEVICENAME\n",
    "            },\n",
    "            \"fields\": {\n",
    "                \"activeKilocalories\": stats_json.get('activeKilocalories'),\n",
    "                \"bmrKilocalories\": stats_json.get('bmrKilocalories'),\n",
    "\n",
    "                'totalSteps': stats_json.get('totalSteps'),\n",
    "                'totalDistanceMeters': stats_json.get('totalDistanceMeters'),\n",
    "\n",
    "                \"highlyActiveSeconds\": stats_json.get(\"highlyActiveSeconds\"),\n",
    "                \"activeSeconds\": stats_json.get(\"activeSeconds\"),\n",
    "                \"sedentarySeconds\": stats_json.get(\"sedentarySeconds\"),\n",
    "                \"sleepingSeconds\": stats_json.get(\"sleepingSeconds\"),\n",
    "                \"moderateIntensityMinutes\": stats_json.get(\"moderateIntensityMinutes\"),\n",
    "                \"vigorousIntensityMinutes\": stats_json.get(\"vigorousIntensityMinutes\"),\n",
    "\n",
    "                \"floorsAscendedInMeters\": stats_json.get(\"floorsAscendedInMeters\"),\n",
    "                \"floorsDescendedInMeters\": stats_json.get(\"floorsDescendedInMeters\"),\n",
    "                \"floorsAscended\": stats_json.get(\"floorsAscended\"),\n",
    "                \"floorsDescended\": stats_json.get(\"floorsDescended\"),\n",
    "\n",
    "                \"minHeartRate\": stats_json.get(\"minHeartRate\"),\n",
    "                \"maxHeartRate\": stats_json.get(\"maxHeartRate\"),\n",
    "                \"restingHeartRate\": stats_json.get(\"restingHeartRate\"),\n",
    "                \"minAvgHeartRate\": stats_json.get(\"minAvgHeartRate\"),\n",
    "                \"maxAvgHeartRate\": stats_json.get(\"maxAvgHeartRate\"),\n",
    "                \n",
    "                \"stressDuration\": stats_json.get(\"stressDuration\"),\n",
    "                \"restStressDuration\": stats_json.get(\"restStressDuration\"),\n",
    "                \"activityStressDuration\": stats_json.get(\"activityStressDuration\"),\n",
    "                \"uncategorizedStressDuration\": stats_json.get(\"uncategorizedStressDuration\"),\n",
    "                \"totalStressDuration\": stats_json.get(\"totalStressDuration\"),\n",
    "                \"lowStressDuration\": stats_json.get(\"lowStressDuration\"),\n",
    "                \"mediumStressDuration\": stats_json.get(\"mediumStressDuration\"),\n",
    "                \"highStressDuration\": stats_json.get(\"highStressDuration\"),\n",
    "                \n",
    "                \"stressPercentage\": stats_json.get(\"stressPercentage\"),\n",
    "                \"restStressPercentage\": stats_json.get(\"restStressPercentage\"),\n",
    "                \"activityStressPercentage\": stats_json.get(\"activityStressPercentage\"),\n",
    "                \"uncategorizedStressPercentage\": stats_json.get(\"uncategorizedStressPercentage\"),\n",
    "                \"lowStressPercentage\": stats_json.get(\"lowStressPercentage\"),\n",
    "                \"mediumStressPercentage\": stats_json.get(\"mediumStressPercentage\"),\n",
    "                \"highStressPercentage\": stats_json.get(\"highStressPercentage\"),\n",
    "                \n",
    "                \"bodyBatteryChargedValue\": stats_json.get(\"bodyBatteryChargedValue\"),\n",
    "                \"bodyBatteryDrainedValue\": stats_json.get(\"bodyBatteryDrainedValue\"),\n",
    "                \"bodyBatteryHighestValue\": stats_json.get(\"bodyBatteryHighestValue\"),\n",
    "                \"bodyBatteryLowestValue\": stats_json.get(\"bodyBatteryLowestValue\"),\n",
    "                \"bodyBatteryDuringSleep\": stats_json.get(\"bodyBatteryDuringSleep\"),\n",
    "                \"bodyBatteryAtWakeTime\": stats_json.get(\"bodyBatteryAtWakeTime\"),\n",
    "                \n",
    "                \"averageSpo2\": stats_json.get(\"averageSpo2\"),\n",
    "                \"lowestSpo2\": stats_json.get(\"lowestSpo2\"),\n",
    "            }\n",
    "        })\n",
    "        if points_list:\n",
    "            logging.info(f\"Success : Fetching daily matrices for date {date_str}\")\n",
    "        return points_list\n",
    "    else:\n",
    "        logging.debug(\"No daily stat data available for the give date \" + date_str)\n",
    "        return []\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_last_sync():\n",
    "    global GARMIN_DEVICENAME\n",
    "    points_list = []\n",
    "    sync_data = garmin_obj.get_device_last_used()\n",
    "    if GARMIN_DEVICENAME_AUTOMATIC:\n",
    "        GARMIN_DEVICENAME = sync_data.get('lastUsedDeviceName') or \"Unknown\"\n",
    "    points_list.append({\n",
    "        \"measurement\":  \"DeviceSync\",\n",
    "        \"time\": datetime.fromtimestamp(sync_data['lastUsedDeviceUploadTime']/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "        \"tags\": {\n",
    "            \"Device\": GARMIN_DEVICENAME\n",
    "        },\n",
    "        \"fields\": {\n",
    "            \"imageUrl\": sync_data.get('imageUrl'),\n",
    "            \"Device\": GARMIN_DEVICENAME\n",
    "        }\n",
    "    })\n",
    "    if points_list:\n",
    "        logging.info(f\"Success : Updated device last sync time\")\n",
    "    else:\n",
    "        logging.warning(\"No associated/synced Garmin device found with your account\")\n",
    "    return points_list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_sleep_data(date_str):\n",
    "    points_list = []\n",
    "    all_sleep_data = garmin_obj.get_sleep_data(date_str)\n",
    "    sleep_json = all_sleep_data.get(\"dailySleepDTO\", None)\n",
    "    if sleep_json[\"sleepEndTimestampGMT\"]:\n",
    "        points_list.append({\n",
    "        \"measurement\":  \"SleepSummary\",\n",
    "        \"time\": datetime.fromtimestamp(sleep_json[\"sleepEndTimestampGMT\"]/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "        \"tags\": {\n",
    "            \"Device\": GARMIN_DEVICENAME\n",
    "            },\n",
    "        \"fields\": {\n",
    "            \"sleepTimeSeconds\": sleep_json.get(\"sleepTimeSeconds\"),\n",
    "            \"deepSleepSeconds\": sleep_json.get(\"deepSleepSeconds\"),\n",
    "            \"lightSleepSeconds\": sleep_json.get(\"lightSleepSeconds\"),\n",
    "            \"remSleepSeconds\": sleep_json.get(\"remSleepSeconds\"),\n",
    "            \"awakeSleepSeconds\": sleep_json.get(\"awakeSleepSeconds\"),\n",
    "            \"averageSpO2Value\": sleep_json.get(\"averageSpO2Value\"),\n",
    "            \"lowestSpO2Value\": sleep_json.get(\"lowestSpO2Value\"),\n",
    "            \"highestSpO2Value\": sleep_json.get(\"highestSpO2Value\"),\n",
    "            \"averageRespirationValue\": sleep_json.get(\"averageRespirationValue\"),\n",
    "            \"lowestRespirationValue\": sleep_json.get(\"lowestRespirationValue\"),\n",
    "            \"highestRespirationValue\": sleep_json.get(\"highestRespirationValue\"),\n",
    "            \"awakeCount\": sleep_json.get(\"awakeCount\"),\n",
    "            \"avgSleepStress\": sleep_json.get(\"avgSleepStress\"),\n",
    "            \"sleepScore\": sleep_json.get(\"sleepScores\", {}).get(\"overall\", {}).get(\"value\"),\n",
    "            \"restlessMomentsCount\": all_sleep_data.get(\"restlessMomentsCount\"),\n",
    "            \"avgOvernightHrv\": all_sleep_data.get(\"avgOvernightHrv\"),\n",
    "            \"bodyBatteryChange\": all_sleep_data.get(\"bodyBatteryChange\"),\n",
    "            \"restingHeartRate\": all_sleep_data.get(\"restingHeartRate\")\n",
    "            }\n",
    "        })\n",
    "    sleep_movement_intraday = all_sleep_data.get(\"sleepMovement\")\n",
    "    if sleep_movement_intraday:\n",
    "        for entry in sleep_movement_intraday:\n",
    "            points_list.append({\n",
    "                \"measurement\":  \"SleepIntraday\",\n",
    "                \"time\": pytz.timezone(\"UTC\").localize(datetime.strptime(entry[\"startGMT\"], \"%Y-%m-%dT%H:%M:%S.%f\")).isoformat(),\n",
    "                \"tags\": {\n",
    "                    \"Device\": GARMIN_DEVICENAME\n",
    "                },\n",
    "                \"fields\": {\n",
    "                    \"SleepMovementActivityLevel\": entry.get(\"activityLevel\",-1),\n",
    "                    \"SleepMovementActivitySeconds\": int((datetime.strptime(entry[\"endGMT\"], \"%Y-%m-%dT%H:%M:%S.%f\") - datetime.strptime(entry[\"startGMT\"], \"%Y-%m-%dT%H:%M:%S.%f\")).total_seconds())\n",
    "                }\n",
    "            })\n",
    "    sleep_levels_intraday = all_sleep_data.get(\"sleepLevels\")\n",
    "    if sleep_levels_intraday:\n",
    "        for entry in sleep_levels_intraday:\n",
    "            if entry.get(\"activityLevel\"):\n",
    "                points_list.append({\n",
    "                    \"measurement\":  \"SleepIntraday\",\n",
    "                    \"time\": pytz.timezone(\"UTC\").localize(datetime.strptime(entry[\"startGMT\"], \"%Y-%m-%dT%H:%M:%S.%f\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"SleepStageLevel\": entry.get(\"activityLevel\"),\n",
    "                        \"SleepStageSeconds\": int((datetime.strptime(entry[\"endGMT\"], \"%Y-%m-%dT%H:%M:%S.%f\") - datetime.strptime(entry[\"startGMT\"], \"%Y-%m-%dT%H:%M:%S.%f\")).total_seconds())\n",
    "                    }\n",
    "                })\n",
    "    sleep_restlessness_intraday = all_sleep_data.get(\"sleepRestlessMoments\")\n",
    "    if sleep_restlessness_intraday:\n",
    "        for entry in sleep_restlessness_intraday:\n",
    "            if entry.get(\"value\"):\n",
    "                points_list.append({\n",
    "                    \"measurement\":  \"SleepIntraday\",\n",
    "                    \"time\": datetime.fromtimestamp(entry[\"startGMT\"]/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"sleepRestlessValue\": entry.get(\"value\")\n",
    "                    }\n",
    "                })\n",
    "    sleep_spo2_intraday = all_sleep_data.get(\"wellnessEpochSPO2DataDTOList\")\n",
    "    if sleep_spo2_intraday:\n",
    "        for entry in sleep_spo2_intraday:\n",
    "            if entry.get(\"spo2Reading\"):\n",
    "                points_list.append({\n",
    "                    \"measurement\":  \"SleepIntraday\",\n",
    "                    \"time\": pytz.timezone(\"UTC\").localize(datetime.strptime(entry[\"epochTimestamp\"], \"%Y-%m-%dT%H:%M:%S.%f\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"spo2Reading\": entry.get(\"spo2Reading\")\n",
    "                    }\n",
    "                })\n",
    "    sleep_respiration_intraday = all_sleep_data.get(\"wellnessEpochRespirationDataDTOList\")\n",
    "    if sleep_respiration_intraday:\n",
    "        for entry in sleep_respiration_intraday:\n",
    "            if entry.get(\"respirationValue\"):\n",
    "                points_list.append({\n",
    "                    \"measurement\":  \"SleepIntraday\",\n",
    "                    \"time\": datetime.fromtimestamp(entry[\"startTimeGMT\"]/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"respirationValue\": entry.get(\"respirationValue\")\n",
    "                    }\n",
    "                })\n",
    "    sleep_heart_rate_intraday = all_sleep_data.get(\"sleepHeartRate\")\n",
    "    if sleep_heart_rate_intraday:\n",
    "        for entry in sleep_heart_rate_intraday:\n",
    "            if entry.get(\"value\"):\n",
    "                points_list.append({\n",
    "                    \"measurement\":  \"SleepIntraday\",\n",
    "                    \"time\": datetime.fromtimestamp(entry[\"startGMT\"]/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"heartRate\": entry.get(\"value\")\n",
    "                    }\n",
    "                })\n",
    "    sleep_stress_intraday = all_sleep_data.get(\"sleepStress\")\n",
    "    if sleep_stress_intraday:\n",
    "        for entry in sleep_stress_intraday:\n",
    "            if entry.get(\"value\"):\n",
    "                points_list.append({\n",
    "                    \"measurement\":  \"SleepIntraday\",\n",
    "                    \"time\": datetime.fromtimestamp(entry[\"startGMT\"]/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"stressValue\": entry.get(\"value\")\n",
    "                    }\n",
    "                })\n",
    "    sleep_bb_intraday = all_sleep_data.get(\"sleepBodyBattery\")\n",
    "    if sleep_bb_intraday:\n",
    "        for entry in sleep_bb_intraday:\n",
    "            if entry.get(\"value\"):\n",
    "                points_list.append({\n",
    "                    \"measurement\":  \"SleepIntraday\",\n",
    "                    \"time\": datetime.fromtimestamp(entry[\"startGMT\"]/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"bodyBattery\": entry.get(\"value\")\n",
    "                    }\n",
    "                })\n",
    "    sleep_hrv_intraday = all_sleep_data.get(\"hrvData\")\n",
    "    if sleep_hrv_intraday:\n",
    "        for entry in sleep_hrv_intraday:\n",
    "            if entry.get(\"value\"):\n",
    "                points_list.append({\n",
    "                    \"measurement\":  \"SleepIntraday\",\n",
    "                    \"time\": datetime.fromtimestamp(entry[\"startGMT\"]/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"hrvData\": entry.get(\"value\")\n",
    "                    }\n",
    "                })\n",
    "    if points_list:\n",
    "        logging.info(f\"Success : Fetching intraday sleep matrices for date {date_str}\")\n",
    "    return points_list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_intraday_hr(date_str):\n",
    "    points_list = []\n",
    "    hr_list = garmin_obj.get_heart_rates(date_str).get(\"heartRateValues\") or []\n",
    "    for entry in hr_list:\n",
    "        if entry[1]:\n",
    "            points_list.append({\n",
    "                    \"measurement\":  \"HeartRateIntraday\",\n",
    "                    \"time\": datetime.fromtimestamp(entry[0]/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"HeartRate\": entry[1]\n",
    "                    }\n",
    "                })\n",
    "    if points_list:\n",
    "        logging.info(f\"Success : Fetching intraday Heart Rate for date {date_str}\")\n",
    "    return points_list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_intraday_steps(date_str):\n",
    "    points_list = []\n",
    "    steps_list = garmin_obj.get_steps_data(date_str)\n",
    "    for entry in steps_list:\n",
    "        if entry[\"steps\"] or entry[\"steps\"] == 0:\n",
    "            points_list.append({\n",
    "                    \"measurement\":  \"StepsIntraday\",\n",
    "                    \"time\": pytz.timezone(\"UTC\").localize(datetime.strptime(entry['startGMT'], \"%Y-%m-%dT%H:%M:%S.%f\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"StepsCount\": entry[\"steps\"]\n",
    "                    }\n",
    "                })\n",
    "    if points_list:\n",
    "        logging.info(f\"Success : Fetching intraday steps for date {date_str}\")\n",
    "    return points_list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_intraday_stress(date_str):\n",
    "    points_list = []\n",
    "    stress_list = garmin_obj.get_stress_data(date_str).get('stressValuesArray') or []\n",
    "    for entry in stress_list:\n",
    "        if entry[1] or entry[1] == 0:\n",
    "            points_list.append({\n",
    "                    \"measurement\":  \"StressIntraday\",\n",
    "                    \"time\": datetime.fromtimestamp(entry[0]/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"stressLevel\": entry[1]\n",
    "                    }\n",
    "                })\n",
    "    bb_list = garmin_obj.get_stress_data(date_str).get('bodyBatteryValuesArray') or []\n",
    "    for entry in bb_list:\n",
    "        if entry[2] or entry[2] == 0:\n",
    "            points_list.append({\n",
    "                    \"measurement\":  \"BodyBatteryIntraday\",\n",
    "                    \"time\": datetime.fromtimestamp(entry[0]/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"BodyBatteryLevel\": entry[2]\n",
    "                    }\n",
    "                })\n",
    "    if points_list:\n",
    "        logging.info(f\"Success : Fetching intraday stress and Body Battery values for date {date_str}\")\n",
    "    return points_list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_intraday_br(date_str):\n",
    "    points_list = []\n",
    "    br_list = garmin_obj.get_respiration_data(date_str).get('respirationValuesArray') or []\n",
    "    for entry in br_list:\n",
    "        if entry[1]:\n",
    "            points_list.append({\n",
    "                    \"measurement\":  \"BreathingRateIntraday\",\n",
    "                    \"time\": datetime.fromtimestamp(entry[0]/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"BreathingRate\": entry[1]\n",
    "                    }\n",
    "                })\n",
    "    if points_list:\n",
    "        logging.info(f\"Success : Fetching intraday Breathing Rate for date {date_str}\")\n",
    "    return points_list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_intraday_hrv(date_str):\n",
    "    points_list = []\n",
    "    hrv_list = (garmin_obj.get_hrv_data(date_str) or {}).get('hrvReadings') or []\n",
    "    for entry in hrv_list:\n",
    "        if entry.get('hrvValue'):\n",
    "            points_list.append({\n",
    "                    \"measurement\":  \"HRV_Intraday\",\n",
    "                    \"time\": pytz.timezone(\"UTC\").localize(datetime.strptime(entry['readingTimeGMT'],\"%Y-%m-%dT%H:%M:%S.%f\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": {\n",
    "                        \"hrvValue\": entry.get('hrvValue')\n",
    "                    }\n",
    "                })\n",
    "    if points_list:\n",
    "        logging.info(f\"Success : Fetching intraday HRV for date {date_str}\")\n",
    "    return points_list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_body_composition(date_str):\n",
    "    points_list = []\n",
    "    body_composition_json = garmin_obj.get_body_composition(date_str).get('totalAverage')\n",
    "    if body_composition_json:\n",
    "        data_fields = {\n",
    "                    \"weight\": body_composition_json[\"weight\"],\n",
    "                    \"bmi\": body_composition_json[\"bmi\"],\n",
    "                    \"bodyFat\": body_composition_json[\"bodyFat\"],\n",
    "                    \"bodyWater\": body_composition_json[\"bodyWater\"],\n",
    "                }\n",
    "        if not all(value is None for value in data_fields.values()):\n",
    "            points_list.append({\n",
    "                    \"measurement\":  \"BodyComposition\",\n",
    "                    \"time\": datetime.fromtimestamp(body_composition_json['from']/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "                    \"tags\": {\n",
    "                        \"Device\": GARMIN_DEVICENAME\n",
    "                    },\n",
    "                    \"fields\": data_fields\n",
    "                })\n",
    "            logging.info(f\"Success : Fetching intraday Body Composition for date {date_str}\")\n",
    "    return points_list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_activity_summary(date_str):\n",
    "    points_list = []\n",
    "    activity_with_gps_id_dict = {}\n",
    "    activity_list = garmin_obj.get_activities_by_date(date_str, date_str)\n",
    "    for activity in activity_list:\n",
    "        if activity.get('hasPolyline'):\n",
    "            activity_with_gps_id_dict[activity.get('activityId')] = activity.get('activityType',{}).get('typeKey', \"Unknown\")\n",
    "        points_list.append({\n",
    "            \"measurement\":  \"ActivitySummary\",\n",
    "            \"time\": datetime.fromtimestamp(activity['beginTimestamp']/1000, tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "            \"tags\": {\n",
    "                \"Device\": GARMIN_DEVICENAME\n",
    "            },\n",
    "            \"fields\": {\n",
    "                'activityId': activity.get('activityId'),\n",
    "                'deviceId': activity.get('deviceId'),\n",
    "                'activityName': activity.get('activityName'),\n",
    "                'activityType': activity.get('activityType',{}).get('typeKey',None),\n",
    "                'distance': activity.get('distance'),\n",
    "                'elapsedDuration': activity.get('elapsedDuration'),\n",
    "                'movingDuration': activity.get('movingDuration'),\n",
    "                'averageSpeed': activity.get('averageSpeed'),\n",
    "                'maxSpeed': activity.get('maxSpeed'),\n",
    "                'calories': activity.get('calories'),\n",
    "                'bmrCalories': activity.get('bmrCalories'),\n",
    "                'averageHR': activity.get('averageHR'),\n",
    "                'maxHR': activity.get('maxHR'),\n",
    "                'locationName': activity.get('locationName'),\n",
    "                'lapCount': activity.get('lapCount'),\n",
    "                'hrTimeInZone_1': activity.get('hrTimeInZone_1'),\n",
    "                'hrTimeInZone_2': activity.get('hrTimeInZone_2'),\n",
    "                'hrTimeInZone_3': activity.get('hrTimeInZone_3'),\n",
    "                'hrTimeInZone_4': activity.get('hrTimeInZone_4'),\n",
    "                'hrTimeInZone_5': activity.get('hrTimeInZone_5'),\n",
    "            }\n",
    "        })\n",
    "        points_list.append({\n",
    "            \"measurement\":  \"ActivitySummary\",\n",
    "            \"time\": datetime.fromtimestamp((activity['beginTimestamp']/1000) + int(activity.get('elapsedDuration')), tz=pytz.timezone(\"UTC\")).isoformat(),\n",
    "            \"tags\": {\n",
    "                \"Device\": GARMIN_DEVICENAME\n",
    "            },\n",
    "            \"fields\": {\n",
    "                'activityId': activity.get('activityId'),\n",
    "                'deviceId': activity.get('deviceId'),\n",
    "                'activityName': \"END\",\n",
    "                'activityType': \"No Activity\",\n",
    "            }\n",
    "        })\n",
    "        logging.info(f\"Success : Fetching Activity summary with id {activity.get('activityId')} for date {date_str}\")\n",
    "    return points_list, activity_with_gps_id_dict"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [],
   "source": [
    "def fetch_activity_GPS(activityIDdict):\n",
    "    points_list = []\n",
    "    for activityID in activityIDdict.keys():\n",
    "        root = ET.fromstring(garmin_obj.download_activity(activityID, dl_fmt=garmin_obj.ActivityDownloadFormat.TCX).decode(\"UTF-8\"))\n",
    "        ns = {\"tcx\": \"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\", \"ns3\": \"http://www.garmin.com/xmlschemas/ActivityExtension/v2\"}\n",
    "        for activity in root.findall(\"tcx:Activities/tcx:Activity\", ns):\n",
    "            activity_type = activityIDdict[activityID]\n",
    "            activity_start_time = datetime.fromisoformat(activity.find(\"tcx:Id\", ns).text.strip(\"Z\"))\n",
    "            lap_index = 1\n",
    "            for lap in activity.findall(\"tcx:Lap\", ns):\n",
    "                lap_start_time = datetime.fromisoformat(lap.attrib.get(\"StartTime\").strip(\"Z\"))\n",
    "                for tp in lap.findall(\".//tcx:Trackpoint\", ns):\n",
    "                    time_obj = datetime.fromisoformat(tp.findtext(\"tcx:Time\", default=None, namespaces=ns).strip(\"Z\"))\n",
    "                    lat = tp.findtext(\"tcx:Position/tcx:LatitudeDegrees\", default=None, namespaces=ns)\n",
    "                    lon = tp.findtext(\"tcx:Position/tcx:LongitudeDegrees\", default=None, namespaces=ns)\n",
    "                    alt = tp.findtext(\"tcx:AltitudeMeters\", default=None, namespaces=ns)\n",
    "                    dist = tp.findtext(\"tcx:DistanceMeters\", default=None, namespaces=ns)\n",
    "                    hr = tp.findtext(\"tcx:HeartRateBpm/tcx:Value\", default=None, namespaces=ns)\n",
    "                    speed = tp.findtext(\"tcx:Extensions/ns3:TPX/ns3:Speed\", default=None, namespaces=ns)\n",
    "\n",
    "                    try: lat = float(lat)\n",
    "                    except: lat = None\n",
    "                    try: lon = float(lon)\n",
    "                    except: lon = None\n",
    "                    try: alt = float(alt)\n",
    "                    except: alt = None\n",
    "                    try: dist = float(dist)\n",
    "                    except: dist = None\n",
    "                    try: hr = float(hr)\n",
    "                    except: hr = None\n",
    "                    try: speed = float(speed)\n",
    "                    except: speed = None\n",
    "\n",
    "                    point = {\n",
    "                        \"measurement\": \"ActivityGPS\",\n",
    "                        \"time\": time_obj.isoformat(), \n",
    "                        \"tags\": {\n",
    "                            \"Device\": GARMIN_DEVICENAME,\n",
    "                            \"ActivityID\": activityID,\n",
    "                            \"ActivitySelector\": activity_start_time.strftime('%Y%m%dT%H%M%SUTC-') + activity_type\n",
    "                        },\n",
    "                        \"fields\": {\n",
    "                            \"ActivityName\": activity_type,\n",
    "                            \"ActivityID\": activityID,\n",
    "                            \"Latitude\": lat,\n",
    "                            \"Longitude\": lon,\n",
    "                            \"Altitude\": alt,\n",
    "                            \"Distance\": dist,\n",
    "                            \"HeartRate\": hr,\n",
    "                            \"Speed\": speed,\n",
    "                            \"lap\": lap_index\n",
    "                        }\n",
    "                    }\n",
    "                    points_list.append(point)\n",
    "                \n",
    "                lap_index += 1\n",
    "        logging.info(f\"Success : Fetching TCX details for activity with id {activityID}\")\n",
    "    return points_list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "def daily_fetch_write(date_str):\n",
    "    write_points_to_influxdb(get_daily_stats(date_str))\n",
    "    write_points_to_influxdb(get_sleep_data(date_str))\n",
    "    write_points_to_influxdb(get_intraday_steps(date_str))\n",
    "    write_points_to_influxdb(get_intraday_hr(date_str))\n",
    "    write_points_to_influxdb(get_intraday_stress(date_str))\n",
    "    write_points_to_influxdb(get_intraday_br(date_str))\n",
    "    write_points_to_influxdb(get_intraday_hrv(date_str))\n",
    "    write_points_to_influxdb(get_body_composition(date_str))\n",
    "    activity_summary_points_list, activity_with_gps_id_dict = get_activity_summary(date_str)\n",
    "    write_points_to_influxdb(activity_summary_points_list)\n",
    "    write_points_to_influxdb(fetch_activity_GPS(activity_with_gps_id_dict))\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def fetch_write_bulk(start_date_str, end_date_str):\n",
    "    logging.info(\"Fetching data for the given period in reverse chronological order\")\n",
    "    time.sleep(3)\n",
    "    write_points_to_influxdb(get_last_sync())\n",
    "    for current_date in iter_days(start_date_str, end_date_str):\n",
    "        success = False\n",
    "        while not success:\n",
    "            try:\n",
    "                daily_fetch_write(current_date)\n",
    "                logging.info(f\"Success : Fatched all available health matries for date {current_date} (skipped any if unavailable)\")\n",
    "                logging.info(f\"Waiting : for {RATE_LIMIT_CALLS_SECONDS} seconds\")\n",
    "                time.sleep(RATE_LIMIT_CALLS_SECONDS)\n",
    "                success = True\n",
    "            except (\n",
    "                    GarminConnectConnectionError,\n",
    "                    GarminConnectAuthenticationError,\n",
    "                    GarminConnectTooManyRequestsError,\n",
    "                    requests.exceptions.HTTPError,\n",
    "                    requests.exceptions.ConnectionError,\n",
    "                    requests.exceptions.Timeout,\n",
    "                    GarthHTTPError) as err:\n",
    "                logging.info(f\"Failed : Failed to fetch one or more matrices for date {current_date}\")\n",
    "                logging.error(err)\n",
    "                logging.info(f\"Waiting : for {FETCH_FAILED_WAIT_SECONDS} seconds\")\n",
    "                time.sleep(FETCH_FAILED_WAIT_SECONDS)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "garmin_obj = garmin_login()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "if MANUAL_START_DATE:\n",
    "    fetch_write_bulk(MANUAL_START_DATE, MANUAL_END_DATE)\n",
    "    logging.info(f\"Bulk update success : Fetched all available health matries for date range {MANUAL_START_DATE} to {MANUAL_END_DATE}\")\n",
    "    exit(0)\n",
    "else:\n",
    "    try:\n",
    "        last_influxdb_sync_time_UTC = pytz.utc.localize(datetime.strptime(list(influxdbclient.query(f\"SELECT * FROM HeartRateIntraday ORDER BY time DESC LIMIT 1\").get_points())[0]['time'],\"%Y-%m-%dT%H:%M:%SZ\"))\n",
    "    except:\n",
    "        logging.warning(\"No previously synced data found in local InfluxDB database, defaulting to 7 day initial fetching. Use specific start date ENV variable to bulk update past data\")\n",
    "        last_influxdb_sync_time_UTC = (datetime.today() - timedelta(days=7)).astimezone(pytz.timezone(\"UTC\"))\n",
    "    \n",
    "    while True:\n",
    "        last_watch_sync_time_UTC = datetime.fromtimestamp(int(garmin_obj.get_device_last_used().get('lastUsedDeviceUploadTime')/1000)).astimezone(pytz.timezone(\"UTC\"))\n",
    "        if last_influxdb_sync_time_UTC < last_watch_sync_time_UTC:\n",
    "            logging.info(f\"Update found : Current watch sync time is {last_watch_sync_time_UTC} UTC\")\n",
    "            fetch_write_bulk(last_influxdb_sync_time_UTC.strftime('%Y-%m-%d'), last_watch_sync_time_UTC.strftime('%Y-%m-%d'))\n",
    "            last_influxdb_sync_time_UTC = last_watch_sync_time_UTC\n",
    "        else:\n",
    "            logging.info(f\"No new data found : Current watch and influxdb sync time is {last_watch_sync_time_UTC} UTC\")\n",
    "        logging.info(f\"waiting for {UPDATE_INTERVAL_SECONDS} seconds before next automatic update calls\")\n",
    "        time.sleep(UPDATE_INTERVAL_SECONDS)\n",
    "        \n",
    "\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "iupred-venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
